#!/usr/bin/perl
# Script to sync Outlook->Google calendar one way, copying only subjects, 
# times, locations, and content (i.e. no recipients, so I won't get 
# scroogled by google calendar sending autoreplies to everyone at work.
#
# What we want is all Outlook events perfectly mirrored to Google (i.e. 
# all Outlook events in Google and no Google events that aren't in 
# Outlook after the sync).  But there is a special category of events 
# (those whose content in Google starts with "TO-OUTLOOK") that will be 
# perfectly mirrored the other way (i.e. all "TO-OUTLOOK" Google events 
# in Outlook and no "TO-OUTLOOK" Outlook events that aren't in Google 
# after the sync).
#
#  NOTE!  This means that if you create Google calendar events you want to keep
#  you need to add the TO-OUTLOOK tag at the very beginning of the body.  
#  Otherwise the event will disappear at the time of the first sync.
#
# Algorithm:  
#   1) Get all Outlook events from 1 week back to 4 weeks out.
#   2) Get all Google events from 1 week back to 4 weeks out.
#   3) Find Google events not labeled "TO-OUTLOOK" with status other than 
#      "canceled" that aren't in Outlook and delete them.
#   4) Find Outlook events labeled "TO-OUTLOOK" that aren't in Google and 
#      delete them.
#   5) Find Outlook events not labeled "TO-OUTLOOK" that aren't in Google 
#      and add them to the Google:0
#      Calendar.
#   6) Find Google events labeled "TO-OUTLOOK" with status other than "canceled"
#      that are not in Outlook and add them to the Outlook Calendar.
#
# Log everything so we can debug anything that goes wrong.
#
#
#
use strict;

BEGIN {$main::TZ = 'US/Mountain';};

##### VERY IMPORTANT
#
# User information and full path information are here.
# You might want to figure out a *more* secure way to do this.  But it works, so good enough for now.
# If you use google 2-step authentication, you need an application specific password here.
#
# The file calsync_googleinfo.pl file needs to be in your Documents folder.
# It should define the scalars $username, $password, and $gcalname that contain the username, password, and calendar name for Google Calendar.
# It should also define $fullpathtoscript, which contains your full path to where you want us to put calsync.pl.logfile.
#
#
use Win32;
my $my_documents_path = Win32::GetFolderPath(Win32::CSIDL_PERSONAL);
my ($username, $password, $gcalname);
eval `type $my_documents_path\\calsync_googleinfo.pl`;

# use all the modules we want.
use Win32::OLE qw(in with);
use Win32::OLE::Const 'Microsoft Outlook';
use Win32::OLE::Variant;

use Date::Manip;
use DateTime::Format::DateManip;

use Net::Google::Calendar;

my $logFile = "calsync.pl.logfile";
if (-e $logFile) {
  open(LF, ">>$logFile") || die "Can't open to write to preexisting file $logFile\n";
} else {
  open(LF, ">$logFile") || die "Can't open to write to nonexistent file $logFile\n";
}
my $today = new Date::Manip::Date;
$today->parse("now");
my $todayDate = $today->printf("%m/%d/%Y");
my $todayTime = $today->printf("%I:%M:%S %p");
print LF "Beginning calendar sync on $todayDate at $todayTime.\n";

# Get an outlook OLE object and initialize what we need.
my $OL = Win32::OLE->GetActiveObject('Outlook.Application') || Win32::OLE->new('Outlook.Application', 'Quit');
my $NameSpace = $OL->GetNameSpace("MAPI");
my $cal = $NameSpace->GetDefaultFolder(olFolderCalendar) or die("error getting cal folder: " . Win32::OLE->LastError());

# We want start and end dates.  Date::Manip is good for calculating relative 
# dates. But we also need them in formats good for outlook (output of justDate) 
# and google (DateTime format)
my $startDate = DateCalc("today", "1 week ago");
my $endDate = DateCalc("today 11:59:59 pm", "4 weeks from now");
my $start_ODate = justDate($startDate);
my $end_ODate = justDate($endDate);
my $start_GDate = DateTime::Format::DateManip->parse_datetime($startDate);
my $end_GDate = DateTime::Format::DateManip->parse_datetime($endDate);

print LF "Syncing dates between " . $start_ODate . " and " . $end_ODate . ".\n";
print LF "Google dates between " . $start_GDate . " and " . $end_GDate . ".\n";

#  1) Get Outlook events in the range.  Store them in a hash of arrays, 
#     tagged by subject and start time.  
print "Getting Outlook events\n";
my $appts = $cal->Items() or die("error getting cal appts: " . Win32::OLE->LastError());
$appts->Sort("[Start]");
$appts->{IncludeRecurrences} = 'True';
my $range = qq{[Start] >= '$start_ODate' and [Start] < '$end_ODate 11:59 pm'};
$appts = $appts->Restrict($range);

my %Outlook_events;
my $tag;
my $Numevents = 0;
for my $appt (in $appts) 
{
	my $subj = $appt->{Subject};
	my $stime = new Date::Manip::Date;
	$stime->parse($appt->{Start});
	$tag = $subj . " \@ " . $appt->{Location} . " \@ " . $stime->printf("%m/%d/%Y %I:%M:%S %p");
        $tag =~ s/\s+/ /g;
	if (!defined($Outlook_events{$tag})) { # Ignore duplicates
		$Outlook_events{$tag}[0] = $appt->{Subject};
		$Outlook_events{$tag}[1] = $appt->{Start};
		$Outlook_events{$tag}[2] = $appt->{End};
		$Outlook_events{$tag}[3] = $appt->{Location};
		$Outlook_events{$tag}[4] = $appt->{Body};
		$Outlook_events{$tag}[5] = $appt->{ReminderMinutesBeforeStart};
		$Numevents++;
	}
}
print "Found $Numevents Outlook events.\n";
#  2) Get Google events in the range.  Store them in a hash of arrays, tagged by subject and start time in Outlook date format.

print "Getting Google events\n";
my $Gcal = Net::Google::Calendar->new;
$Gcal->login( $username, $password ) || die "Can't establish connectivity with Google Calendar\n";

my @Gcalendars = $Gcal->get_calendars;

my $calFound = 0;
for my $gc (@Gcalendars) {
  if ($gc->title eq $gcalname) {
    $calFound = 1;
    $Gcal->set_calendar($gc);
    last;
  }
}
if (!$calFound) {
  print LF "\nError: Could not find calendar $gcalname\nExiting\n";
  exit 1;
}  
my %opts;
$opts{"max-results"} = 5000; # avoid the 25 event default limit
$opts{"start-min"} = $start_GDate;
$opts{"start-max"} = $end_GDate;
my @events = $Gcal->get_events(%opts);
my %Google_events;
my $startD;
my $endD;
$Numevents = @events;
print "Found $Numevents Google events.\n";
for my $event (@events) {
  if ($event->status eq "canceled") { next; } # Skip canceled events.
  my ($beginning, $ending) = $event->when();
  $startD = DateTime::Format::DateManip->format_datetime($beginning);
  $endD = DateTime::Format::DateManip->format_datetime($ending);
  my ($yy, $mm, $dd) = splitDate($startD);
  my ($hh, $mn) = splitTime($startD);
  my $ampm = ($hh > 11) ? "PM" : "AM";
  if ($hh > 12) { $hh -= 12; };
  my $subj = $event->title;
  my $stime = new Date::Manip::Date;
  $stime->parse($startD);
  $tag = $subj . " \@ " . $event->location() . " \@ " . $stime->printf("%m/%d/%Y %I:%M:%S %p");
  $tag =~ s/\s+/ /g;
  if (!defined($Google_events{$tag})) { # Again, ignore duplicates
    $Google_events{$tag}[0] = $event->title;
    $Google_events{$tag}[1] = $startD;
    $Google_events{$tag}[2] = $endD;
    $Google_events{$tag}[3] = $event->location();
    $Google_events{$tag}[4] = $event->content()->body;
  }
}

#   3) Find Google events not labeled "TO-OUTLOOK" with status other than 
#      "canceled" that aren't in Outlook and delete them.

print LF "\nChecking for Google calendar events that need deleting...\n";
my $key;
foreach $key (keys %Google_events) {
  if ($Google_events{$key}[4] =~ /^TO\-OUTLOOK/) {
	  #print LF "Not deleting event <$key> labeled TO-OUTLOOK.\n";
    next;
  }
  if (exists($Outlook_events{$key})) {
	  #print LF "Not deleting event <$key> that is also in Outlook.\n";
    next;
  }
  print LF "Deleting event <$key> that is not in Outlook.\n";
  for my $event (@events) {
    if ($event->status eq "canceled") { next; }
    my ($beginning, $ending) = $event->when();
    $startD = DateTime::Format::DateManip->format_datetime($beginning);
    $endD = DateTime::Format::DateManip->format_datetime($ending);
    my ($yy, $mm, $dd) = splitDate($startD);
    my ($hh, $mn) = splitTime($startD);
    my $ampm = ($hh > 11) ? "PM" : "AM";
    if ($hh > 12) { $hh -= 12; };
    my $subj = $event->title;
    my $stime = new Date::Manip::Date;
    $stime->parse("$mm/$dd/$yy $hh:$mn:00 $ampm");
    $tag = $subj . " \@ " . $event->location() . " \@ " . $stime->printf("%m/%d/%Y %I:%M:%S %p");
    $tag =~ s/\s+/ /g;
    if ($tag eq $key) {
	    ($Gcal->delete_entry($event));
    }
  }
}
print LF "\n";

#   4) Find Outlook events labeled "TO-OUTLOOK" that aren't in Google and 
#      delete them.

print LF "\nChecking for TO-OUTLOOK Outlook calendar events that need deleting...\n";
my $key;
foreach $key (keys %Outlook_events) {
  if ($Outlook_events{$key}[4] !~ /^TO\-OUTLOOK/) { next; } # Skip "normal" events
  if (exists($Google_events{$key})) {
	  #print LF "Not deleting event <$key> that is also in Google.\n";
    next;
  }
  print LF "Deleting event <$key><$Outlook_events{$key}[4]> that is not in Google\n";
  for my $appt (in $appts) 
  {
	my $subj = $appt->{Subject};
	my $stime = new Date::Manip::Date;
	$stime->parse($appt->{Start});
	$tag = $subj . " \@ " . $appt->{Location} . " \@ " . $stime->printf("%m/%d/%Y %I:%M:%S %p");
        $tag =~ s/\s+/ /g;
	if ($tag eq $key) { 
	  ($appt->Delete);
        }
  }
}
print LF "\n";

#   5) Find Outlook events not labeled "TO-OUTLOOK" that aren't in Google 
#      and add them to the Google Calendar.

print LF "\nChecking for non-TO-OUTLOOK events in Outlook that aren't in Google and adding them to Google...\n";
my $key;
foreach $key (keys %Outlook_events) {
  if ($Outlook_events{$key}[4] =~ /^TO\-OUTLOOK/) { next; }
  if (!exists($Google_events{$key})) {
    print LF "Adding event <$key> to Google.\n";
    #print LF "  Subject: $Outlook_events{$key}[0]\n";
    #print LF "  From: $Outlook_events{$key}[1], To: $Google_events{$key}[2]\n";
    #print LF "  Location: $Outlook_events{$key}[3]\n";
    #print LF "  Body: $Outlook_events{$key}[4]\n";
    my $ne = new Net::Google::Calendar::Entry;
    $ne->title($Outlook_events{$key}[0]);
    $ne->when(DateTime::Format::DateManip->parse_datetime($Outlook_events{$key}[1]),DateTime::Format::DateManip->parse_datetime($Outlook_events{$key}[2]));
    $ne->location($Outlook_events{$key}[3]);
    $ne->content($Outlook_events{$key}[4]);
    $ne->reminder("alert", "minutes", $Outlook_events{$key}[5]);
    $Gcal->add_entry($ne);
  }
}
print LF "\n";

#   6) Find Google events labeled "TO-OUTLOOK" with status other than "canceled"
#      that are not in Outlook and add them to the Outlook Calendar.

print LF "\nChecking for TO-OUTLOOK events in Google that aren't in Outlook and adding them to Outlook...\n";
my $key;
foreach $key (keys %Google_events) {
  if ($Google_events{$key}[4] !~ /^TO\-OUTLOOK/) { next; }
  if (!exists($Outlook_events{$key})) {
    print LF "Adding event <$key> to Outlook.\n";
    #print LF "  Subject: $Google_events{$key}[0]\n";
    #print LF "  From: $Google_events{$key}[1], To: $Google_events{$key}[2]\n";
    #print LF "  Location: $Google_events{$key}[3]\n";
    #print LF "  Body: $Google_events{$key}[4]\n";
    my $ne = $OL->CreateItem(olAppointmentItem);
    $ne->{MeetingStatus} = olMeeting;
    $ne->{Subject} = $Google_events{$key}[0];
    my $starttime = new Date::Manip::Date;
    $starttime->parse($Google_events{$key}[1]);
    #print LF "Event <$key> start time is " . $starttime->printf("%m/%d/%Y %H:%M") . ".\n";
    $ne->{Start} = $starttime->printf("%m/%d/%Y %H:%M");
    my $endtime = new Date::Manip::Date;
    $endtime->parse($Google_events{$key}[2]);
    #print LF "Event <$key> end time is " . $endtime->printf("%m/%d/%Y %I:%M:%S %p") . ".\n";
    $ne->{End} = $endtime->printf("%m/%d/%Y %H:%M");
    $ne->{Location} = $Google_events{$key}[3];
    $ne->{Body} = $Google_events{$key}[4];
    $ne->{ReminderMinutesBeforeStart} = 15;
    $ne->Save;
  }
}
print LF "\n\n\n";


sub justTime
{
    my (undef, $time, $ampm) = split(/\s/, $_[0], 3);
    $time = substr($time, 0, -3);  # chop off seconds
    $time =~ s{^ (\d) :}{ $1:}x;

    return join(' ', $time, $ampm);
}

# for datemanip date format:
sub DayOfWeek
{
    my $shortdate = $_[0];
    $shortdate =~ s/-//g;
    my ($y, $m, $d) = splitDate($shortdate);
    return [qw(Sun Mon Tue Wed Thu Fri Sat)]->[Date_DayOfWeek($m, $d, $y)];
}
# for datemanip date format:
sub justDate
{
    join('-', splitDate($_[0]));
}
# for datemanip date format:
sub splitDate
{
    return (substr($_[0], 0, 4),
            substr($_[0], 4, 2),
            substr($_[0], 6, 2));
}
# for datemanip date format:
sub splitTime
{
    return (substr($_[0], 8, 2),
            substr($_[0], 11, 2));
}

